package ci.web.router;

import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Locale;
import java.util.TimeZone;

import ci.web.HttpMethod;
import ci.web.core.CiContext;
import ci.web.core.CiRequest;
import ci.web.core.CiResponse;
import io.netty.handler.codec.http.HttpHeaderNames;

/**
 * 路由-静态文件控制器
 * @author zhh
 */
public class CiFile implements CiCall{

    //文件目录
    private final String dir;
    //请求路由
    private final String path;
    //文件缓存时间(秒)
    private final int maxAge;
    
    private final String[] pathArr;
    private final String[] exceptExtendArr;
    
    /**
     * @param dir       文件目录
     */
    public CiFile(String dir){
        this(dir, "/");
    }
    /**
     * @param dir       文件目录
     * @param maxAge    文件缓存时间(秒)
     */
    public CiFile(String dir, int maxAge){
        this(dir, "/", maxAge);
    }
    /**
     * @param dir       文件目录
     * @param path      请求路由
     */
    public CiFile(String dir, String path){
        this(dir, path, 604800);
    }
    /**
     * @param dir       文件目录
     * @param path      请求路由
     * @param maxAge    文件缓存时间(秒)
     */
    public CiFile(String dir, String path, int maxAge) {
        this(dir, path, 604800, null);
    }
    /**
     * @param dir           文件目录
     * @param path          请求路由
     * @param maxAge        文件缓存时间(秒)
     * @param exceptExtend  排除的文件后缀
     */
    public CiFile(String dir, String path, int maxAge, String exceptExtend) {
        this.dir = formatDir(dir);
        this.maxAge = maxAge;
        
        //build-path-match
        StringBuilder sb = new StringBuilder();
        String[] arr = path.split("[;,]");
        HashSet<String> set = new HashSet<String>();
        for(int i=0;i<arr.length;i++){
            String p = arr[i];
            if(p.isEmpty())continue;
            p = formatPath(p);
            set.add(p);
            sb.append(p);
        }
        this.path = sb.toString();
        if(set.size()>1){
            pathArr = set.toArray(new String[set.size()]);
        }else{
            pathArr = null;
        }
        
        //build- extends-math
        set.clear();
        if(exceptExtend!=null && !exceptExtend.isEmpty()){
            arr = exceptExtend.trim().split("[;,]");
            for(String e : arr){
                if(e.isEmpty())continue;
                set.add(e);
            }
        }
        if(set.size()>0){
            this.exceptExtendArr = set.toArray(new String[set.size()]);
        }else{
            this.exceptExtendArr = null;
        }
    }
    /**
     * 是否匹配文件路由
     * @param ctx
     * @return
     */
    public boolean match(CiContext ctx) {
        if(ctx.method()!=HttpMethod.GET && ctx.method()!=HttpMethod.HEAD){
            return false;
        }
        String p = ctx.path();
        boolean d = p.charAt(p.length()-1)=='/';
        if(p.length()>1 && d==false && p.lastIndexOf('.')<1){
            return false;
        }
        //排除某些后缀
        if(d==false && exceptExtendArr != null){
            for(String ext:exceptExtendArr){
                if(p.endsWith(ext)){
                    return false;
                }
            }
        }
        if(pathArr==null){
            if(p.startsWith(path)){
                if(path.length()==1){
                    //不建议根路径配置方式，容易引起性能问题
                    if(d){
                        p = p+"index.html";
                    }
                    return new File(dir+p).exists();
                }
                return true;
            }
        }else{
            for(String n :pathArr){
                if(p.startsWith(n)){
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * 调用处理
     */
    @Override
    public boolean call(CiContext ctx) {
        doWith(ctx);
        return true;
    }
    private void doWith(CiContext context) {

        CiRequest request = context.in();
        CiResponse response = context.out();

        File file = new File(sanitizeUri(dir, request.path()));
        if (file.isHidden() || !file.exists()) {
            response.sendError(NOT_FOUND.code(), NOT_FOUND.reasonPhrase());
            return;
        }
        if (file.isDirectory() || !file.isFile()) {
            response.sendError(FORBIDDEN.code(), FORBIDDEN.reasonPhrase());
            return;
        }
        if (isModified(context, file) == false) {
            sendNotModified(context);
        } else {
            context.send(file, maxAge);
        }
    }

    /**
     * 缓存文件是否改变
     * @param request
     * @param file
     * @return
     */
    public static boolean isModified(CiRequest request, File file){
        return request.headers().contains(HttpHeaderNames.IF_MODIFIED_SINCE) && 
                isModified(request, file.lastModified());
    }
    /**
     * 请求头的缓存是否发生改变
     * @param request
     * @param fileLastModified
     * @return
     */
    public static boolean isModified(CiRequest request, long fileLastModified){
        // Cache Validation
        String ifModifiedSince = request.getHeader(HttpHeaderNames.IF_MODIFIED_SINCE);
        if (ifModifiedSince != null && !ifModifiedSince.isEmpty()) {
            Date ifModifiedSinceDate = null;
            try {
                ifModifiedSinceDate = parseGmtTime(ifModifiedSince);
            } catch (ParseException e) {
                
            }

            // Only compare up to the second because the datetime format we send to the client
            // does not have milliseconds
            if(ifModifiedSinceDate!=null){
                return ifModifiedSinceDate.getTime()==fileLastModified;
            }
        }
        return true;
    }
    
    /**
     * 写文件未改变的缓存响应header头(304)
     * @param response
     */
    public static void sendNotModified(CiResponse response) {
        response.setStatus(NOT_MODIFIED.code(), NOT_MODIFIED.reasonPhrase());
        response.setHeader(HttpHeaderNames.DATE, formatGmtTime(new GregorianCalendar().getTime()));
    }

    /**
     * 写-文件日期以及缓存控制-header头
     * @param response
     * @param fileToCache
     * @param fileCacheSecond
     */
    public static void sendDateAndCache(CiResponse response, File fileToCache, int fileCacheSecond) {
        sendDateAndCache(response, new Date(fileToCache.lastModified()), fileCacheSecond);
    }
    /**
     * 写-文件日期以及缓存控制-响应header头
     * @param response
     * @param lastModified
     * @param fileCacheSecond
     */
    public static void sendDateAndCache(CiResponse response, Date lastModified, int fileCacheSecond) {
        // Date header
        Date date = new GregorianCalendar().getTime();
        response.setHeader(HttpHeaderNames.DATE, formatGmtTime(date));
        response.setHeader(HttpHeaderNames.EXPIRES, formatGmtTime(date));
        response.setHeader(HttpHeaderNames.LAST_MODIFIED, formatGmtTime(lastModified));
        response.setHeader(HttpHeaderNames.CACHE_CONTROL, "private, max-age=" + fileCacheSecond);
    }

    /**
     * GMT时间格式-时间戳
     */
    private static ThreadLocal<DateFormat> GmtDateFormat = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
            format.setTimeZone(TimeZone.getTimeZone("GMT"));
            return format;
        }
    };
    /**
     * 格式化GMT时间
     * @param date
     * @return
     */
    public static String formatGmtTime(Date date){
        return GmtDateFormat.get().format(date);
    }
    /**
     * 解析GMT时间
     * @param date
     * @return
     * @throws ParseException
     */
    public static Date parseGmtTime(String date) throws ParseException{
        return GmtDateFormat.get().parse(date);
    }
    
    //格式化路径
    private static String sanitizeUri(String dir, String uri) {
        // Decode the path.
        if(dir==null)return null;
        try {
            uri = URLDecoder.decode(uri, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            try {
                uri = URLDecoder.decode(uri, "ISO-8859-1");
            } catch (UnsupportedEncodingException e1) {
            }
        }
        if(uri.charAt(uri.length()-1)=='/'){
            uri = uri+"index.html";
        }
        // Convert file separators.
        uri = uri.replace('/', File.separatorChar);
        // Simplistic dumb security check.
        // You will have to do something serious in the production environment.
        if (uri.contains(File.separator + ".") ||
            uri.contains("." + File.separator) ||
            uri.startsWith(".") || uri.endsWith(".")) {
            return null;
        }

        // Convert to absolute path.
//        int p_idx = uri.indexOf('?');
//        if(p_idx>=0){
//            uri = uri.substring(0, p_idx);
//        }
        if(uri.isEmpty()){
            uri = "index.html";
        }
        return dir + uri;
    }
    
    /**
     * 格式化路由路径
     * @param p
     * @return
     */
    private static String formatPath(String p) {
        if(p.charAt(0)!='/'){
            p = '/'+p;
        }
        if(p.charAt(p.length()-1)!='/' && p.indexOf('.')<0){
            p = p+'/';
        }
        return p;
    }
    /**
     * 格式化文件夹路径
     * @param d
     * @return
     */
    private static String formatDir(String d) {
        return (d.endsWith("/") || d.endsWith("\\")) ? d.substring(0, d.length()-1):d;
    }
}
